W13. Дженерики Java, параметризация типов, вариантность, wildcard-типы
1. Резюме
1.1 Введение в дженерики
1.1.1 Идея genericity
Generics (в C++ близко к templates, в функциональных языках — parametric polymorphism) позволяют писать код для разных типов с сохранением проверки типов. Суть — parameterize классы, интерфейсы и методы параметром типа и иметь одну реализацию для многих конкретных типов.
Дженерики — это «шаблон», который потом заполняют конкретными типами: вместо ListOfPersons, ListOfCars, ListOfBooks с копипастой логики — один List<T>.
Дженерики orthogonal к наследованию:
- Inheritance — специализация и абстракция (
OrderedListextendsList);
- Generics — параметризация типа (
List<Person>vsList<Car>).
Их можно сочетать: обобщённый класс может стоять в иерархии наследования.
1.1.2 Зачем нужны дженерики
Без них типичны проблемы:
- Code duplication — почти одинаковые классы на каждый тип;
- нарушение DRY;
- потеря compile-time проверки типов при использовании универсального
Object.
Дженерики распространены почти во всех современных языках:
- Generics: Ada, Delphi, Eiffel, Java, Scala, C#, Swift, Rust
- Templates: C++, D
- Parametric polymorphism: ML, Scala, Haskell
1.2 Жизнь без дженериков
1.2.1 Дублирование кода
Раньше для «типобезопасных» коллекций писали отдельный класс под каждый тип:
class ListOfPersons {
void extend(Person v) { ... }
void remove(Person v) { ... }
}
class ListOfCars {
void extend(Car v) { ... }
void remove(Car v) { ... }
}Алгоритмы extend и remove одинаковы — отличаются только типы; это ломает DRY и усложняет сопровождение.
1.2.2 Универсальный тип
Обходной путь — «универсальный» тип, куда можно класть что угодно.
C++ Approach: void*
class ListOfAnything {
void extend(void* v) { ... }
void remove(void* v) { ... }
};Любой указатель приводится к void*, но проверка типов исчезает:
ListOfAnything lst;
lst.extend(new Car()); // OK
lst.extend(new Person()); // Compiles, but is this intended?
lst.remove(new City()); // Also compiles — no type safety!Java: базовый тип Object
В Java Object — общий предок ссылочных типов:
public class List {
public void extend(Object item) { ... }
public Object elem(int i) { ... }
}List lst = new List();
lst.extend(new MyType());
MyType v = (MyType)lst.elem(5); // Explicit cast required!У такого подхода серьёзные недостатки:
- нельзя зафиксировать тип элементов на этапе компиляции;
- компилятор не проверяет согласованность типов;
- нужны явные приведения при извлечении;
- риск ошибок времени выполнения при неверном cast.
1.3 Boxing и unboxing
1.3.1 Проблема примитивов
В Java два вида типов:
- Reference types: классы, интерфейсы, массивы (наследники
Object); - Value types:
int,double,boolean,char, … (примитивы, неObject).
Примитив нельзя положить в List<Object> без упаковки.
1.3.2 Wrapper-классы
Для каждого примитива в java.lang есть wrapper:
| Тип значения | Wrapper |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
boolean |
Boolean |
char |
Character |
Каждый wrapper хранит одно значение соответствующего примитива:
Integer i = new Integer(1);
Double d = new Double(0.5);1.3.3 Операции boxing / unboxing
Boxing — автоматическое преобразование примитива в wrapper:
List lst = new List();
lst.extend(1); // int -> Integer (boxing)
// Equivalent to: lst.extend(new Integer(1));Unboxing — автоматическое извлечение примитива из wrapper:
int i = (int)lst.elem(1); // Integer -> int (unboxing)Без дженериков boxing/unboxing дороже и легче поймать runtime-ошибку:
List lst3 = new List();
lst3.extend(new MyType());
int j = (int)lst3.elem(2); // Runtime error! MyType is not Integer1.4 Обобщённые (generic) классы
1.4.1 Объявление generic-класса
Generic class объявляется с одним или несколькими type parameters в угловых скобках:
class List<T> {
void extend(T v) { ... }
void remove(T v) { ... }
T elem(int i) { ... }
}Здесь T — type parameter (формальный / универсальный параметр типа), условно «любой тип». Класс List<T> — абстракция-шаблон.
Соглашения об именах параметров типа — одна заглавная буква:
T— TypeE— ElementK— KeyV— ValueN— Number
1.4.2 Инстанцирование
Чтобы использовать generic-класс, подставьте actual type argument:
List<Car> garage = new List<Car>();
garage.extend(new Car()); // OK
garage.extend(new Person()); // Compile-time error!Компилятор подставляет Car вместо T и получается типобезопасный List<Car>.
Diamond operator <> (JDK 7+): справа можно не повторять тип:
List<Car> garage = new List<>(); // Diamond operator <>Вывод типа с var (JDK 10+) для локальных переменных:
var ints = new List<Integer>(); // Compiler infers List<Integer>1.4.3 Плюсы generic-классов
Дженерики устраняют перечисленные проблемы:
List<MyType> lst1 = new List<MyType>();
lst1.extend(new MyType());
MyType v = lst1.elem(1); // No cast needed!
List<Integer> lst2 = new List<>();
lst2.extend(1); // No explicit boxing needed
int i = lst2.elem(1); // No explicit unboxing needed
lst2.extend(new MyType()); // Compile-time error!
List<MyType> lst3 = new List<>();
lst3.extend(new MyType());
int j = (int)lst3.elem(3); // Compile-time error: illegal conversionПлюсы:
- Type safety — нельзя положить «чужой» тип в коллекцию;
- No code duplication — одна реализация на все типы;
- Compile-time checking — ошибки до запуска;
- No explicit casting — компилятор знает тип элементов;
- для ссылочных типов меньше лишнего boxing/unboxing.
1.4.4 Несколько параметров типа
У generic-класса может быть несколько параметров типа:
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}Инстанцирование:
Pair<String, Integer> p1 = new OrderedPair<>("Even", 8);
Pair<String, String> p2 = new OrderedPair<>("hello", "world");В аргументах типа можно использовать уже параметризованные типы:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<>());1.5 Обобщённые методы
1.5.1 Объявление
Generic methods вводят собственные параметры типа, независимые от класса; область видимости — метод:
class Lists {
public static <T> T sort(List<T> lst) {
// ...
}
}<T> перед типом результата делает метод generic. Пишут <T> T sort(...), а не T sort<T>(...) — иначе синтаксис Java ломается.
Generic могут быть и статические, и методы экземпляра:
public class Test {
static <T> void genericDisplay(T element) {
System.out.println(element.getClass().getName() + " = " + element);
}
public static void main(String[] args) {
genericDisplay(11); // T inferred as Integer
genericDisplay("data flair"); // T inferred as String
genericDisplay(1.0); // T inferred as Double
}
}1.5.2 Вызов
Можно явно указать аргументы типа:
boolean same = Util.<Integer, String>compare(p1, p2);Или положиться на вывод типов (чаще так):
boolean same = Util.compare(p1, p2); // Types inferred from arguments1.6 Ограниченные (bounded) параметры типа
1.6.1 Зачем ограничивать
Иногда нужно запретить произвольный аргумент типа. Пример:
class Garage<T> {
void repair(T vehicle) { ... }
}
Garage<Personal> myCars = new Garage<Personal>(); // OK
Garage<Bus> busStation = new Garage<Bus>(); // OK
Garage<Frog> lake = new Garage<Frog>(); // Compiles, but makes no sense!«Гараж лягушек» семантически бессмыслен, а lake.repair() может привести к ошибкам времени выполнения.
1.6.2 Верхняя граница extends
Ограничьте параметр типа сверху:
class Garage<T extends Vehicle> {
void repair(T vehicle) { ... }
}Теперь T — только Vehicle или подкласс:
Garage<Personal> myCars = new Garage<Personal>(); // OK
Garage<Bus> busStation = new Garage<Bus>(); // OK
Garage<Frog> lake = new Garage<Frog>(); // Compile-time error!1.6.3 Граница-интерфейс
extends работает и для интерфейсов:
interface iAccount {
int getId();
}
class Bank<T extends iAccount> {
T[] accounts;
public Bank(T[] accs) { this.accounts = accs; }
}Теперь T должен реализовать iAccount.
1.6.4 Несколько границ
Несколько границ через &:
class Bank<T1, T2 extends Person & iAccount> {
// T1 has no restrictions
// T2 must extend Person AND implement iAccount
}Правила:
- You can specify multiple interfaces
- You can specify at most ONE class
- If there’s a class, it must come first:
<T extends SomeClass & Interface1 & Interface2>
C# Comparison: C# uses where clauses for a similar effect:
public class MyTemplate<Type1, Type2>
where Type1 : IComparable,
where Type2 : MyInterface, MyBaseClass
{ ... }1.7 Реализация дженериков: C++ и Java
1.7.1 Модель C++ (размножение кода)
В C++ для каждой инстанциации получается отдельная копия кода с подставленными типами:
List<int>generates one version of the codeList<string>generates another version
Плюсы: лучше оптимизировать. Минусы: раздувание кода (больше объёма).
1.7.2 Модель Java (type erasure)
В Java для всех инстанциаций один и тот же байткод; информация о типах стирается на этапе компиляции, при необходимости добавляется boxing:
List<Integer>andList<String>use the same bytecode
Плюсы: компактнее. Минусы: boxing/unboxing и потеря части информации о типах в runtime.
1.8 Принцип подстановки Лисков (LSP)
1.8.1 Подтип
Subtype — если типы связаны extends или implements:
Integeris a subtype ofNumberDoubleis a subtype ofNumber
1.8.2 Формулировка
Liskov Substitution Principle (LSP):
- переменной типа
Tможно присвоить значение любого подтипаT; - в параметр типа
Tможно передать аргумент любого подтипаT.
Это связано с dynamic types: метод, ждущий Animal, примет Lion, Frog, …
List<Number> nums = new List<Number>();
nums.extend(2); // Integer is a subtype of Number — OK
nums.extend(3.14); // Double is a subtype of Number — OK1.9 Вариантность
1.9.1 Вопрос вариантности
Пусть есть два класса:
class Base { ... }
class Derived extends Base { ... }And a generic collection:
class Collection<T> { ... }Вопрос: как связаны Collection<Base> и Collection<Derived>?
1.9.2 Три варианта
Возможны три отношения:
- Invariance:
Collection<Base>andCollection<Derived>have NO relationship (typical for Java generics) - Covariance:
Collection<Derived>is a subtype ofCollection<Base>(intuitive, but not always safe) - Contravariance:
Collection<Base>is a subtype ofCollection<Derived>(counterintuitive, but useful in some cases)
1.9.3 Почему covariance опасна
Предположим covariance: List<Integer> — подтип List<Number>:
List<Integer> ints = new List<Integer>();
ints.extend(1);
ints.extend(2);
List<Number> nums = ints; // If covariant, this would be legal
nums.extend(3.14); // Adding a Double to a List<Integer>!Проблема: в список Integer попал Double. Вывод: List<Integer> не подтип List<Number>.
1.9.4 Почему contravariance опасна
Предположим contravariance: List<Integer> — супертип List<Number>:
List<Number> nums = new List<Number>();
nums.extend(2.78);
nums.extend(3.14);
List<Integer> ints = nums; // If contravariant, this would be legal
Integer x = ints.elem(0); // Getting a Double as Integer!Проблема: Double читают как Integer. Вывод: List<Integer> не супертип List<Number>.
1.9.5 Инвариантность Java
List<Integer> и List<Number> invariant — между ними нет отношения подтипа, хотя Integer extends Number.
Важное исключение: массивы — Integer[] является подтипом Number[], что даёт ArrayStoreException в runtime.
1.9.6 Наследование между generic-классами
Наследование шаблонов классов при фиксированном аргументе типа по-прежнему работает:
class Collection<T> { ... }
class List<T> extends Collection<T> { ... }
Collection<Integer> col;
List<Integer> lst = new List<Integer>();
col = lst; // OK! List<Integer> IS a subtype of Collection<Integer>Инвариантность относится к аргументу типа, а не к иерархии List extends Collection.
1.10 Wildcard-типы
1.10.1 Зачем ?
Как написать метод для «любой коллекции»?
Без дженериков (старый стиль):
void printCollection(Collection c) {
Iterator i = c.iterator();
for (int k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}Наивный дженерик (не подходит):
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}Так принимается только Collection<Object>, а не Collection<String> / Collection<Integer> из-за инвариантности!
1.10.2 Неограниченный wildcard
Wildcard ? — «неизвестный тип»:
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}Collection<?> принимает любую коллекцию элементов; эквивалентно Collection<? extends Object>.
1.10.3 ? extends (верхняя граница)
Upper bounded wildcard — неизвестный тип не выше заданного:
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}<? extends Number> подходит для Number, Integer, Double, Long, …:
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li)); // sum = 6.0
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld)); // sum = 7.01.10.4 ? super (нижняя граница)
Lower bounded wildcard — неизвестный тип не ниже заданного:
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}<? super Integer> — Integer, Number или Object.
1.10.5 Wildcards в сигнатурах
Wildcard помогает обойти инвариантность в API:
class List<T> {
// Accept lists of T or any subtype of T
public void addAnotherList(List<? extends T> newLst) { ... }
// Accept lists of T or any supertype of T
public void addAnotherList2(List<? super T> newLst) { ... }
}1.10.6 PECS: Producer Extends, Consumer Super
PECS — памятка по выбору wildcard:
- Producer Extends: только чтение из коллекции —
? extends T; - Consumer Super: только запись в коллекцию —
? super T.
Пример:
// Producer: we READ from source
public void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) { // Reading from source
dest.add(item); // Writing to dest
}
}1.11 Плюсы дженериков Java
Кратко:
Compile-time type safety — ошибки ловятся при компиляции.
Elimination of casts — не нужны лишние приведения при
get.// Without generics List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); // Cast required // With generics List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // No cast neededCode reuse — одна реализация для многих типов.
Generic algorithms — типобезопасные алгоритмы над коллекциями.
2. Определения
- Generics: возможность параметризовать классы, интерфейсы и методы типами и писать типобезопасный переиспользуемый код.
- Type Parameter (Type Variable): формальный параметр типа (
T,E,K,Vи т.д.) в объявлении дженерика. - Actual Type Argument: конкретный тип при инстанцировании или вызове (например
IntegerвList<Integer>). - Instantiation (of generics): получение конкретного типа из обобщённого при подстановке аргументов типа.
- Boxing: автоматическое упаковывание примитива в соответствующий wrapper (
int→Integer). - Unboxing: автоматическое извлечение примитива из wrapper (
Integer→int). - Wrapper Classes: классы в
java.lang, оборачивающие примитивы (Integer,Double,Boolean, …). - Bounded Type Parameter: ограничение параметра типа с помощью
extends/super. - Upper Bound: верхняя граница через
extends— тип и его подтипы. - Lower Bound: нижняя граница через
super— тип и его супертипы. - Wildcard:
?— «неизвестный» тип в сочетании с границами. - Variance: как связаны обобщённые типы
G<A>иG<B>при связиAиB. - Invariance: между
G<A>иG<B>нет отношения подтипа, даже еслиAиBсвязаны (типично для Java-коллекций). - Covariance: подтип аргумента «наследуется» вверх по конструктору типа (интуитивно, но не всегда безопасно).
- Contravariance: отношение подтипов «переворачивается» на уровне
G<·>. - Type Erasure: в JVM параметры типа стираются и заменяются
Objectили границами. - Diamond Operator: синтаксис
<>(JDK 7+), чтобы компилятор вывел аргументы типа. - Type Inference: вывод аргументов типа компилятором из контекста.
- Liskov Substitution Principle (LSP): подтип можно подставить вместо супертипа без нарушения контракта.
- PECS: «Producer Extends, Consumer Super» — эвристика выбора
? extends/? superпри чтении и записи. - Raw Type: использование
Listбез<...>— теряется статическая проверка типов.
3. Примеры
3.1. Ошибка компиляции generic-класса (Лаба 12, Задание 1)
Скомпилируется ли класс? Если нет — почему?
public final class Algorithm {
public static <T> T max(T x, T y) {
return x > y ? x : y;
}
}Нажмите, чтобы увидеть решение
Ключевая идея: оператор > не определён для произвольного параметра типа T.
- Analysis of the code:
- The class declares a generic method
maxwith type parameterT - The method attempts to compare
xandyusing the>operator
- The class declares a generic method
- The problem:
- The
>operator only works with primitive numeric types (int,double, etc.) - Generic type
Tcould be any reference type (e.g.,String,Person) - You cannot use
>to compare arbitrary objects
- The
- The solution:
- To compare generic objects, use
Comparableinterface:
- To compare generic objects, use
public final class Algorithm {
public static <T extends Comparable<T>> T max(T x, T y) {
return x.compareTo(y) > 0 ? x : y;
}
}Ответ: нет, не скомпилируется: > нельзя применить к T. Ограничьте T extends Comparable<T> и используйте compareTo().
3.2. Медиатека с дженериками и без (Лаба 12, Задание 1)
Спроектируйте «библиотеку» для книги, видео и газеты: одна версия с дженериками и одна без. Для хранения можно использовать любые подходящие API.
Нажмите, чтобы увидеть решение
Ключевая идея: дженерики дают проверку типов и убирают лишние приведения.
Version WITHOUT Generics:
import java.util.ArrayList;
import java.util.List;
// Media classes
class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
@Override
public String toString() {
return "Book: " + title + " by " + author;
}
}
class Video {
private String title;
private int duration;
public Video(String title, int duration) {
this.title = title;
this.duration = duration;
}
@Override
public String toString() {
return "Video: " + title + " (" + duration + " min)";
}
}
class Newspaper {
private String name;
private String date;
public Newspaper(String name, String date) {
this.name = name;
this.date = date;
}
@Override
public String toString() {
return "Newspaper: " + name + " - " + date;
}
}
// Library WITHOUT generics
class MediaLibrary {
private List items = new ArrayList(); // Raw type - no type safety
public void addItem(Object item) {
items.add(item);
}
public Object getItem(int index) {
return items.get(index);
}
public void displayAll() {
for (Object item : items) {
System.out.println(item);
}
}
public int size() {
return items.size();
}
}Проблемы версии без дженериков:
- No type safety: can add any object type
- Requires explicit casting when retrieving items
- Runtime errors if wrong cast is used
Version WITH Generics:
import java.util.ArrayList;
import java.util.List;
// Generic Library class
class GenericMediaLibrary<T> {
private List<T> items = new ArrayList<>();
public void addItem(T item) {
items.add(item);
}
public T getItem(int index) {
return items.get(index);
}
public void displayAll() {
for (T item : items) {
System.out.println(item);
}
}
public int size() {
return items.size();
}
}
// Usage example
public class Main {
public static void main(String[] args) {
// Without generics - no type safety
MediaLibrary oldLibrary = new MediaLibrary();
oldLibrary.addItem(new Book("1984", "George Orwell"));
oldLibrary.addItem(new Video("Matrix", 136));
oldLibrary.addItem("Random String"); // Compiles but wrong!
Book b = (Book) oldLibrary.getItem(0); // Cast required
// With generics - type safe
GenericMediaLibrary<Book> bookLibrary = new GenericMediaLibrary<>();
bookLibrary.addItem(new Book("1984", "George Orwell"));
bookLibrary.addItem(new Book("Brave New World", "Aldous Huxley"));
// bookLibrary.addItem(new Video("Matrix", 136)); // Compile error!
Book book = bookLibrary.getItem(0); // No cast needed
GenericMediaLibrary<Video> videoLibrary = new GenericMediaLibrary<>();
videoLibrary.addItem(new Video("Matrix", 136));
videoLibrary.addItem(new Video("Inception", 148));
System.out.println("=== Book Library ===");
bookLibrary.displayAll();
System.out.println("\n=== Video Library ===");
videoLibrary.displayAll();
}
}Ответ: GenericMediaLibrary<T> проверяет типы на этапе компиляции, убирает cast и не даст положить «чужой» тип.
3.3. Метод с верхней границей wildcard (Лаба 12, Задание 2)
Скомпилируется ли метод? Если нет — почему?
public static void print(List<? extends Number> list) {
for (Number n : list) {
System.out.print(n + " ");
}
System.out.println();
}Нажмите, чтобы увидеть решение
Ключевая идея: ? extends позволяет безопасно читать элементы как верхнюю границу.
- Analysis of the code:
- The method accepts
List<? extends Number>— a list ofNumberor any subtype - The loop iterates using
Number n, which is valid because all elements are guaranteed to be at leastNumber
- The method accepts
- Why this works:
? extends Numbermeans the list contains elements of some type that extendsNumber- Since all subtypes of
Numbercan be assigned to aNumbervariable, the iteration is type-safe - The loop can safely read elements as
Number
Ответ: да, метод корректен: List<? extends Number> читается как поток Number, все элементы — как минимум Number.
3.4. Иерархия животных и PECS (Лаба 12, Задание 2)
Класс Animal с полем nickname и методом voice(). Классы Cat и Dog с полями purLoudness и barkingLoudness, переопределите voice().
Класс Main с методами displayAnimals, makeTalk, addAnimals. Отдельные множества животных, кошек и собак; вызовите методы.
Подсказка: PECS (producer extends, consumer super); для Set корректно переопределите hashCode() и equals().
Нажмите, чтобы увидеть решение
Ключевая идея: PECS задаёт, ? extends или ? super — в зависимости от чтения или записи.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
// Base Animal class
class Animal {
protected String nickname;
public Animal(String nickname) {
this.nickname = nickname;
}
public String getNickname() {
return nickname;
}
public void voice() {
System.out.println(nickname + " makes a sound");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Animal animal = (Animal) o;
return Objects.equals(nickname, animal.nickname);
}
@Override
public int hashCode() {
return Objects.hash(nickname);
}
}
// Cat class
class Cat extends Animal {
private int purLoudness;
public Cat(String nickname, int purLoudness) {
super(nickname);
this.purLoudness = purLoudness;
}
@Override
public void voice() {
System.out.println(nickname + " purrs with loudness " + purLoudness + ": Purrr~");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Cat cat = (Cat) o;
return purLoudness == cat.purLoudness;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), purLoudness);
}
}
// Dog class
class Dog extends Animal {
private int barkingLoudness;
public Dog(String nickname, int barkingLoudness) {
super(nickname);
this.barkingLoudness = barkingLoudness;
}
@Override
public void voice() {
System.out.println(nickname + " barks with loudness " + barkingLoudness + ": Woof!");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
Dog dog = (Dog) o;
return barkingLoudness == dog.barkingLoudness;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), barkingLoudness);
}
}
// Main class demonstrating PECS
public class Main {
// PRODUCER: reads from the set (extends)
// We only READ animals from the set to display them
public static void displayAnimals(Set<? extends Animal> animals) {
System.out.println("--- Displaying animals ---");
for (Animal animal : animals) {
System.out.println("Animal: " + animal.getNickname());
}
}
// PRODUCER: reads from the set (extends)
// We only READ animals from the set to make them talk
public static void makeTalk(Set<? extends Animal> animals) {
System.out.println("--- Animals talking ---");
for (Animal animal : animals) {
animal.voice();
}
}
// CONSUMER: writes to the set (super)
// We WRITE animals to the destination set
public static void addAnimals(Set<? super Cat> dest, Set<? extends Cat> source) {
System.out.println("--- Adding cats to collection ---");
for (Cat cat : source) {
dest.add(cat);
System.out.println("Added: " + cat.getNickname());
}
}
public static void main(String[] args) {
// Create sets
Set<Animal> animals = new HashSet<>();
Set<Cat> cats = new HashSet<>();
Set<Dog> dogs = new HashSet<>();
// Add elements to cats set
cats.add(new Cat("Whiskers", 3));
cats.add(new Cat("Mittens", 5));
cats.add(new Cat("Luna", 2));
// Add elements to dogs set
dogs.add(new Dog("Rex", 8));
dogs.add(new Dog("Buddy", 6));
dogs.add(new Dog("Max", 9));
// Add some animals directly
animals.add(new Animal("Generic Pet"));
// Display different sets using displayAnimals
System.out.println("=== Cats ===");
displayAnimals(cats); // Works: Set<Cat> matches Set<? extends Animal>
System.out.println("\n=== Dogs ===");
displayAnimals(dogs); // Works: Set<Dog> matches Set<? extends Animal>
System.out.println("\n=== All Animals ===");
displayAnimals(animals); // Works: Set<Animal> matches Set<? extends Animal>
// Make animals talk
System.out.println("\n=== Cats talking ===");
makeTalk(cats);
System.out.println("\n=== Dogs talking ===");
makeTalk(dogs);
// Add cats to animals set using PECS
System.out.println("\n=== Adding cats to animals set ===");
addAnimals(animals, cats); // animals accepts ? super Cat, cats produces ? extends Cat
System.out.println("\n=== Updated Animals Set ===");
displayAnimals(animals);
makeTalk(animals);
}
}Пояснение к использованию PECS:
displayAnimals(Set<? extends Animal>)— PRODUCER EXTENDS- We only read from the set
- The set “produces” animals for us to display
? extends AnimalallowsSet<Cat>,Set<Dog>, orSet<Animal>
makeTalk(Set<? extends Animal>)— PRODUCER EXTENDS- We only read animals to call their
voice()method - Same reasoning as above
- We only read animals to call their
addAnimals(Set<? super Cat> dest, Set<? extends Cat> source)— BOTHdestis a CONSUMER (we write to it) → use? super Catsourceis a PRODUCER (we read from it) → use? extends Cat
Ответ: при чтении — ? extends, при записи — ? super; для Set переопределены equals/hashCode.
3.5. Статические члены и дженерики (Лаба 12, Задание 3)
Скомпилируется ли метод? Если нет — почему?
public class Singleton<T> {
private static T instance = null;
public static T getInstance() {
if (instance == null) {
instance = new Singleton<T>();
}
return instance;
}
}Нажмите, чтобы увидеть решение
Ключевая идея: параметр типа класса нельзя использовать в static-полях и static-методах.
- The problem:
Tis a type parameter that belongs to instances of the class- Static members belong to the class itself, not to any particular instance
- Different instances might have different type arguments (
Singleton<String>,Singleton<Integer>) - But there’s only one copy of static members shared by all instances
- Why this doesn’t work:
private static T instance— Cannot useTin a static field declarationpublic static T getInstance()— Cannot useTas return type of a static methodnew Singleton<T>()— Cannot create instance with type parameter in static context
- Additional error:
- Even if generics were allowed,
instance = new Singleton<T>()would be wrong becausegetInstance()should returnT, notSingleton<T>
- Even if generics were allowed,
Ответ: нет: T в static недопустим — статика одна на все инстансы, а T разный. Для singleton с типом нужен другой шаблон (например Class<T>).
3.6. Ветклиника с дженериками (Лаба 12, Задание 3)
Простая ветклиника: у питомца id (уникален), nickname и owner (не обязаны быть уникальны). Храните животных в Map<Integer, Animal>. Типы: кошки (purLoudness), змеи (venomDanger), кролики (earLength). Владелец: name, surname, age.
Класс VeterinaryClinic с displayPets, addPets. Дважды вызовите addPets. Попробуйте разных животных с одинаковым id — что должно произойти?
Подсказка: PECS (producer extends, consumer super)
Нажмите, чтобы увидеть решение
Ключевая идея: Map с дженериками и поведение при дублирующемся ключе.
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
// Owner class
class Owner {
private String name;
private String surname;
private int age;
public Owner(String name, String surname, int age) {
this.name = name;
this.surname = surname;
this.age = age;
}
public String getName() { return name; }
public String getSurname() { return surname; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " " + surname + " (age: " + age + ")";
}
}
// Base Pet class
abstract class Pet {
protected int id;
protected String nickname;
protected Owner owner;
public Pet(int id, String nickname, Owner owner) {
this.id = id;
this.nickname = nickname;
this.owner = owner;
}
public int getId() { return id; }
public String getNickname() { return nickname; }
public Owner getOwner() { return owner; }
public abstract String getSpeciesInfo();
@Override
public String toString() {
return String.format("ID: %d, Name: %s, Owner: %s, %s",
id, nickname, owner, getSpeciesInfo());
}
}
// Cat class
class Cat extends Pet {
private int purLoudness;
public Cat(int id, String nickname, Owner owner, int purLoudness) {
super(id, nickname, owner);
this.purLoudness = purLoudness;
}
@Override
public String getSpeciesInfo() {
return "Cat (pur loudness: " + purLoudness + ")";
}
}
// Snake class
class Snake extends Pet {
private int venomDanger;
public Snake(int id, String nickname, Owner owner, int venomDanger) {
super(id, nickname, owner);
this.venomDanger = venomDanger;
}
@Override
public String getSpeciesInfo() {
return "Snake (venom danger level: " + venomDanger + ")";
}
}
// Rabbit class
class Rabbit extends Pet {
private double earLength;
public Rabbit(int id, String nickname, Owner owner, double earLength) {
super(id, nickname, owner);
this.earLength = earLength;
}
@Override
public String getSpeciesInfo() {
return "Rabbit (ear length: " + earLength + " cm)";
}
}
// Veterinary Clinic class
class VeterinaryClinic {
private Map<Integer, Pet> pets = new HashMap<>();
// PRODUCER: reads from the source map (extends)
public void displayPets(Map<Integer, ? extends Pet> petMap) {
System.out.println("=== Clinic Pets Registry ===");
if (petMap.isEmpty()) {
System.out.println("No pets registered.");
return;
}
for (Map.Entry<Integer, ? extends Pet> entry : petMap.entrySet()) {
System.out.println(entry.getValue());
}
System.out.println("Total pets: " + petMap.size());
}
// CONSUMER for dest (super), PRODUCER for source (extends)
public void addPets(Map<Integer, ? super Pet> dest,
Map<Integer, ? extends Pet> source) {
System.out.println("\n--- Adding pets ---");
for (Map.Entry<Integer, ? extends Pet> entry : source.entrySet()) {
int id = entry.getKey();
Pet pet = entry.getValue();
// Check if ID already exists
if (dest.containsKey(id)) {
System.out.println("WARNING: Pet with ID " + id +
" already exists! Replacing: " + dest.get(id).toString());
}
dest.put(id, pet);
System.out.println("Added: " + pet.getNickname() + " (ID: " + id + ")");
}
}
// Convenience method to add to internal pets map
public void addPets(Map<Integer, ? extends Pet> source) {
addPets(this.pets, source);
}
// Display internal pets
public void displayAllPets() {
displayPets(this.pets);
}
public Map<Integer, Pet> getPets() {
return pets;
}
}
// Main class
public class Main {
public static void main(String[] args) {
// Create owners
Owner alice = new Owner("Alice", "Smith", 28);
Owner bob = new Owner("Bob", "Johnson", 35);
Owner carol = new Owner("Carol", "Williams", 42);
// Create veterinary clinic
VeterinaryClinic clinic = new VeterinaryClinic();
// First batch of pets
Map<Integer, Pet> batch1 = new HashMap<>();
batch1.put(1, new Cat(1, "Whiskers", alice, 5));
batch1.put(2, new Snake(2, "Slinky", bob, 3));
batch1.put(3, new Rabbit(3, "Fluffy", carol, 12.5));
System.out.println("===== FIRST BATCH =====");
clinic.addPets(batch1);
clinic.displayAllPets();
// Second batch of pets - including duplicate ID!
Map<Integer, Pet> batch2 = new HashMap<>();
batch2.put(4, new Cat(4, "Mittens", alice, 3));
batch2.put(5, new Snake(5, "Viper", bob, 8));
batch2.put(3, new Rabbit(3, "Snowball", carol, 10.0)); // Duplicate ID!
System.out.println("\n===== SECOND BATCH (with duplicate ID 3) =====");
clinic.addPets(batch2);
clinic.displayAllPets();
// Demonstrate with specific pet type maps
System.out.println("\n===== Adding specific type maps =====");
Map<Integer, Cat> catMap = new HashMap<>();
catMap.put(6, new Cat(6, "Luna", alice, 4));
catMap.put(7, new Cat(7, "Simba", bob, 6));
// This works because of ? extends Pet
clinic.addPets(catMap);
clinic.displayAllPets();
}
}Вывод:
===== FIRST BATCH =====
--- Adding pets ---
Added: Whiskers (ID: 1)
Added: Slinky (ID: 2)
Added: Fluffy (ID: 3)
=== Clinic Pets Registry ===
ID: 1, Name: Whiskers, Owner: Alice Smith (age: 28), Cat (pur loudness: 5)
ID: 2, Name: Slinky, Owner: Bob Johnson (age: 35), Snake (venom danger level: 3)
ID: 3, Name: Fluffy, Owner: Carol Williams (age: 42), Rabbit (ear length: 12.5 cm)
Total pets: 3
===== SECOND BATCH (with duplicate ID 3) =====
--- Adding pets ---
Added: Mittens (ID: 4)
Added: Viper (ID: 5)
WARNING: Pet with ID 3 already exists! Replacing: ID: 3, Name: Fluffy, Owner: Carol Williams (age: 42), Rabbit (ear length: 12.5 cm)
Added: Snowball (ID: 3)
=== Clinic Pets Registry ===
...
Total pets: 5
Что происходит при повторе ID:
если добавить питомца с ключом, который уже есть в Map, Map.put() заменяет старое значение новым. В показанной реализации выводится предупреждение и выполняется замена.
Ответ: для чтения из Map — ? extends Pet, для записи — ? super Pet; при том же ключе put заменяет значение.
3.7. Generic-класс и Comparable (Лаба 12, Задание 4)
Рассмотрите класс:
class Node<T> implements Comparable<T> {
public int compareTo(T obj) { /* ... */ }
// ...
}Скомпилируется ли фрагмент? Если нет — почему?
Node<String> node = new Node<>();
Comparable<String> comp = node;Нажмите, чтобы увидеть решение
Ключевая идея: generic-класс может реализовать generic-интерфейс с согласованными параметрами; отношения подтипов сохраняются.
- Analysis:
Node<T>implementsComparable<T>- When instantiated as
Node<String>, it implementsComparable<String> - A variable of type
Comparable<String>can hold any object that implementsComparable<String>
- Why this works:
Node<String>IS-AComparable<String>(by theimplementsclause)- The assignment
comp = nodeis a standard upcast - This is Liskov Substitution Principle in action
Ответ: да: Node<String> — подтип Comparable<String> по контракту implements.
3.8. Реализация стека (generic) (Лаба 12, Задание 4 — по желанию)
Набросайте generic-стек (LIFO): сигнатуры push, pop, isEmpty.
Нажмите, чтобы увидеть решение
Ключевая идея: стек — LIFO; параметризуется типом элемента.
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.List;
/**
* A generic Stack implementation using LIFO (Last-In-First-Out) principle.
* @param <T> the type of elements in this stack
*/
public class Stack<T> {
private List<T> elements;
/**
* Creates an empty stack.
*/
public Stack() {
elements = new ArrayList<>();
}
/**
* Pushes an element onto the top of this stack.
* @param item the element to push
*/
public void push(T item) {
elements.add(item);
}
/**
* Removes and returns the element at the top of this stack.
* @return the element at the top of this stack
* @throws EmptyStackException if this stack is empty
*/
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
/**
* Returns the element at the top without removing it.
* @return the element at the top of this stack
* @throws EmptyStackException if this stack is empty
*/
public T peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.get(elements.size() - 1);
}
/**
* Tests if this stack is empty.
* @return true if this stack contains no elements, false otherwise
*/
public boolean isEmpty() {
return elements.isEmpty();
}
/**
* Returns the number of elements in this stack.
* @return the number of elements
*/
public int size() {
return elements.size();
}
}
// Usage example
class Main {
public static void main(String[] args) {
Stack<Integer> intStack = new Stack<>();
intStack.push(1);
intStack.push(2);
intStack.push(3);
while (!intStack.isEmpty()) {
System.out.println(intStack.pop()); // Prints: 3, 2, 1
}
Stack<String> stringStack = new Stack<>();
stringStack.push("Hello");
stringStack.push("World");
System.out.println(stringStack.pop()); // Prints: World
}
}Сводка сигнатур методов:
public void push(T item)— adds element to the toppublic T pop()— removes and returns top elementpublic boolean isEmpty()— checks if stack is empty
Ответ: Stack<T> хранит элементы типа T; операции push(T), pop() → T, isEmpty() → boolean.
3.9. Словарь (generic) (Лаба 12, Задание 5 — по желанию)
Набросайте generic Dictionary: get, put, isEmpty, keys, values; последние два — параметризованные коллекции.
Нажмите, чтобы увидеть решение
Ключевая идея: словарь — пары ключ–значение как у HashMap, с параметрами K и V.
import java.util.*;
/**
* A generic Dictionary implementation for storing key-value pairs.
* @param <K> the type of keys maintained by this dictionary
* @param <V> the type of mapped values
*/
public class Dictionary<K, V> {
private Map<K, V> data;
/**
* Creates an empty dictionary.
*/
public Dictionary() {
data = new HashMap<>();
}
/**
* Returns the value associated with the specified key.
* @param key the key whose associated value is to be returned
* @return the value associated with the key, or null if not found
*/
public V get(K key) {
return data.get(key);
}
/**
* Associates the specified value with the specified key.
* @param key the key with which the value is to be associated
* @param value the value to be associated with the key
* @return the previous value associated with the key, or null
*/
public V put(K key, V value) {
return data.put(key, value);
}
/**
* Removes the mapping for the specified key.
* @param key the key whose mapping is to be removed
* @return the previous value associated with the key, or null
*/
public V remove(K key) {
return data.remove(key);
}
/**
* Returns true if this dictionary contains no key-value mappings.
* @return true if empty, false otherwise
*/
public boolean isEmpty() {
return data.isEmpty();
}
/**
* Returns a collection view of the keys in this dictionary.
* @return a Set of keys
*/
public Set<K> keys() {
return data.keySet();
}
/**
* Returns a collection view of the values in this dictionary.
* @return a Collection of values
*/
public Collection<V> values() {
return data.values();
}
/**
* Returns the number of key-value mappings.
* @return the size of the dictionary
*/
public int size() {
return data.size();
}
/**
* Returns true if this dictionary contains the specified key.
* @param key the key to check
* @return true if the key exists, false otherwise
*/
public boolean containsKey(K key) {
return data.containsKey(key);
}
}
// Usage example
class Main {
public static void main(String[] args) {
// Dictionary of student names to grades
Dictionary<String, Integer> grades = new Dictionary<>();
grades.put("Alice", 95);
grades.put("Bob", 87);
grades.put("Carol", 92);
System.out.println("Alice's grade: " + grades.get("Alice")); // 95
System.out.println("Is empty: " + grades.isEmpty()); // false
System.out.println("All students: " + grades.keys()); // [Alice, Bob, Carol]
System.out.println("All grades: " + grades.values()); // [95, 87, 92]
// Dictionary of product IDs to prices
Dictionary<Integer, Double> prices = new Dictionary<>();
prices.put(1001, 29.99);
prices.put(1002, 49.99);
for (Integer productId : prices.keys()) {
System.out.println("Product " + productId + ": $" + prices.get(productId));
}
}
}Сводка сигнатур методов:
public V get(K key)— retrieves value by keypublic V put(K key, V value)— stores key-value pairpublic boolean isEmpty()— checks if dictionary is emptypublic Set<K> keys()— returns collection of all keyspublic Collection<V> values()— returns collection of all values
Ответ: Dictionary<K,V> разделяет тип ключа и значения; keys() → Set<K>, values() → Collection<V>.
3.10. Generic-класс Box (Туториал 12, Задание 1)
Создайте generic Box, хранящий значение произвольного типа.
Нажмите, чтобы увидеть решение
Ключевая идея: перевод «сырого» класса на generic-версию ради типобезопасности.
Non-generic version (problematic):
public class Box {
private Object object;
public void setObject(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
}
// Usage - requires casting
Box box = new Box();
box.setObject("Hello");
String s = (String) box.getObject(); // Cast required!
box.setObject(123); // No compile error, but mixing typesGeneric version (type-safe):
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
private T t;
public void setObject(T t) {
this.t = t;
}
public T getObject() {
return t;
}
}
// Usage - no casting needed
Box<String> stringBox = new Box<>();
stringBox.setObject("Hello");
String s = stringBox.getObject(); // No cast!
// stringBox.setObject(123); // Compile error! Type safety!
Box<Integer> intBox = new Box<>();
intBox.setObject(42);
int value = intBox.getObject(); // Auto-unboxingОтвет: Box<T> фиксирует тип хранимого значения и убирает ненужные cast.
3.11. Generic-метод для сравнения Pair (Туториал 12, Задание 2)
Напишите generic-метод, сравнивающий два объекта Pair.
Нажмите, чтобы увидеть решение
Ключевая идея: у generic-метода свои параметры типа, независимые от класса.
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
public class Util {
// Generic method with type parameters K and V
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
// Usage
public class Main {
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
Pair<Integer, String> p3 = new Pair<>(1, "apple");
// Explicit type specification
boolean same1 = Util.<Integer, String>compare(p1, p2);
System.out.println("p1 equals p2: " + same1); // false
// Type inference (compiler infers types from arguments)
boolean same2 = Util.compare(p1, p3);
System.out.println("p1 equals p3: " + same2); // true
}
}Ответ: метод <K,V> boolean compare(...) вводит собственные K,V; типы выводятся из аргументов или задаются явно.
3.12. Ограниченные дженерики для чисел (Туториал 12, Задание 3)
Напишите метод, работающий только с числовыми типами.
Нажмите, чтобы увидеть решение
Ключевая идея: граница extends ограничивает множество типов и открывает методы границы.
import java.util.Arrays;
import java.util.List;
public class NumberUtils {
// Upper bounded: T must be Number or its subtype
public static <T extends Number> List<T> fromArrayToList(T[] a) {
return Arrays.asList(a);
}
// Can use Number methods inside the method
public static <T extends Number> double sum(List<T> numbers) {
double total = 0.0;
for (T number : numbers) {
total += number.doubleValue(); // Can call Number methods!
}
return total;
}
// Using wildcards instead
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
s += n.doubleValue();
}
return s;
}
}
// Usage
public class Main {
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
List<Integer> intList = NumberUtils.fromArrayToList(intArray);
System.out.println("Sum: " + NumberUtils.sum(intList)); // 15.0
List<Double> doubleList = Arrays.asList(1.5, 2.5, 3.5);
System.out.println("Sum: " + NumberUtils.sumOfList(doubleList)); // 7.5
// This would not compile:
// String[] strArray = {"a", "b"};
// NumberUtils.fromArrayToList(strArray); // Error: String doesn't extend Number
}
}Ответ: при T extends Number доступны числовые типы и методы вроде doubleValue().